/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package avl.sv.shared.sources;

import avl.sv.shared.ImageID;
import avl.sv.shared.Rect;
import avl.sv.shared.SlideReference;
import avl.tiff.*;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author benbryan
 */
public abstract class SlideSource {

    private final ArrayList<TiffDirectoryBuffer> tdBuffs = new ArrayList<>();
    private int imageDimX, imageDimY;    
    ExecutorService pool;  
    private SlideReference slideReference;
    private Callable postBufferFillEvent;
    AtomicInteger activeDownloadCount = new AtomicInteger(0);
    final int maxActiveDownloads = 10;
    TreeMap<Double,TiffDirectoryBuffer> zoomMap = new TreeMap<>();

    public SlideSource(SlideReference slideReference) {
        this.slideReference = slideReference;
        pool = Executors.newFixedThreadPool(maxActiveDownloads);
    }

    public ArrayList<TiffDirectoryBuffer> getTiffDirectoryBuffers() {
        return tdBuffs;
    }   
    
    protected abstract BufferedImage downloadTile(TiffDirectoryBuffer dir, int i, int j);
    
    public byte[] getHash() {
        return slideReference.hash;
    }
    
    public String getSlideSetName() {
        return slideReference.slideSetName;
    }

    public SlideReference getSlideReference() {
        return slideReference;
    }
    
    public String getImageName() {
        return slideReference.slideName;
    }
    
    public int getImageDimX() {
        return imageDimX;
    }
    public int getImageDimY() {
        return imageDimY;
    }
    
    public TreeMap<Double, TiffDirectoryBuffer> getZoomMap() {
        return zoomMap;
    }
        
    public void setPostBufferFillEvent(Callable postBufferFillEvent){
        this.postBufferFillEvent = postBufferFillEvent;
    }    
    
    protected void parseDBuff(){
        imageDimX = 0; imageDimY = 0;    
        for (int i = 0; i < tdBuffs.size(); i++){
            TiffDirectoryBuffer d = tdBuffs.get(i);
            imageDimX = Math.max(imageDimX, d.getImageWidth());
            imageDimY = Math.max(imageDimY, d.getImageLength());
            Double z = d.getZoomLevel();
            if ((!z.isInfinite())&&(!z.isNaN())){
                zoomMap.put(z, d);
            }
        }
        if ((zoomMap.isEmpty()) && (tdBuffs.size()>0)){
            zoomMap.put(1d, tdBuffs.get(0));
        }
    }
        
    public void removeOldTiles(long ageInMilliseconds){
        for (TiffDirectoryBuffer d:tdBuffs){
            d.removeOldTiles(ageInMilliseconds);
        }
    }  
    
    public void clearBuffers(){
        for (TiffDirectoryBuffer d:tdBuffs){
            d.clearBuffers();
        }
    }  

    public TiffDirectoryBuffer getThumbnailDirectoryBuffer(){
        return zoomMap.firstEntry().getValue();
    }    
    public TiffDirectoryBuffer getBaseDirectoryBuffer(){
        return zoomMap.lastEntry().getValue();
    }
        
    public int decrementAndGetActiveDownloadCount(){
        return activeDownloadCount.decrementAndGet();
    }
           
    public BufferedImage getSubImage(Rectangle bounds, TiffDirectoryBuffer dir ) {
        BufferedImage img = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_3BYTE_BGR);
        Graphics2D g = (Graphics2D) img.getGraphics();
        
        Rect r = new Rect(bounds.x, bounds.y, bounds.x+bounds.width, bounds.y+bounds.height);
        g.translate(-r.getLx(),-r.getLy());
        Point tilesToPlot[] = findTilesRect(dir, r);
        
        CompletionService<FillBuffer2> completionService = new ExecutorCompletionService<>(pool);
        
        int width = dir.getTileWidth();
        int height = dir.getTileLength(); 
        int numelToDo = 0;
        for (Point tileToPlot : tilesToPlot) {
            int w = tileToPlot.x;
            int l = tileToPlot.y;
            if (dir.getTile(w, l) == null){
                dir.setTileFechingStatus(w, l, BufferingStatus.BUFFERING);
                completionService.submit(new FillBuffer2(tileToPlot.x, tileToPlot.y, dir));
                numelToDo++;
            } else { 
                g.drawImage(dir.getTile(w, l), w * width, l * height, width, height, null);
            }
        }
        
        boolean errors = false;
        while ((numelToDo > 0) && !errors) {
            try {
                Future<FillBuffer2> resultFuture = completionService.take(); //blocks if none available
                FillBuffer2 buffer = resultFuture.get();
                numelToDo--;    
                int w = buffer.w;
                int l = buffer.l;
                g.drawImage(dir.getTile(w, l), w * width, l * height, width, height, null);
            } catch (Exception e) {
                errors = true;
            }
        }
        return img;
    }
    
    public void fillBuffers(Point[] tilesToPlot, TiffDirectoryBuffer dir){
        ArrayList<Point> temp = new ArrayList<>();
        for (Point tileToPlot : tilesToPlot) {
            int w = tileToPlot.x;
            int l = tileToPlot.y;
            if (dir.getTileFechingStatus(w, l) == BufferingStatus.NOT_BUFFERED){
                temp.add(new Point(w, l));
            }
        }     
        dir.setTilesInView(temp);
        startBuffering();
    }
    
    protected void startBuffering(){
        if (activeDownloadCount.get() >= maxActiveDownloads) { return;}        
        Iterator<TiffDirectoryBuffer> dirIt = zoomMap.values().iterator();
        while(dirIt.hasNext()){
            TiffDirectoryBuffer dir = dirIt.next();
            ArrayList<Point> inView = dir.getTilesInView();
            if (inView == null){ continue;}
            for (Point tile:inView){
                int w = tile.x;
                int l = tile.y;
                if (dir.getTileFechingStatus(w, l) == BufferingStatus.NOT_BUFFERED){
                    dir.setTileFechingStatus(w, l, BufferingStatus.BUFFERING);
                    activeDownloadCount.incrementAndGet();
                    Callable fbCall = new FillBuffer(w, l, dir);
                    pool.submit(fbCall);
                    if (activeDownloadCount.get() >= maxActiveDownloads) { return;}
                }
            }
        }
    }
    
    private class FillBuffer2 implements Callable{
        int w, l;
        TiffDirectoryBuffer dir;
        public FillBuffer2(int w, int l, TiffDirectoryBuffer dir) {
            this.w = w;
            this.l = l;
            this.dir = dir;
        }
        @Override
        public Object call() {
            try {
                downloadTile(dir, w, l);
            } catch (Exception ex) {
                dir.setTileFechingStatus(w, l, BufferingStatus.NOT_BUFFERED);
                Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
            }
            return this;
        }
    }

    private class FillBuffer implements Callable{
        int w, l;
        TiffDirectoryBuffer dir;
        public FillBuffer(int w, int l, TiffDirectoryBuffer dir) {
            this.w = w;
            this.l = l;
            this.dir = dir;
        }
        @Override
        public Object call() {
            try {
                downloadTile(dir, w, l);
                if (postBufferFillEvent != null){
                    postBufferFillEvent.call();
                }
            } catch (Exception ex) {
                dir.setTileFechingStatus(w, l, BufferingStatus.NOT_BUFFERED);
                Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
                startBuffering();
            } 
            return this;
        }       
    }

    public void paintTiles(TiffDirectoryBuffer dir, Graphics2D g, Point tilesToPlot[]){
        if (tilesToPlot == null){return;}
        fillBuffers(tilesToPlot, dir);
        int width = dir.getTileWidth();
        int height = dir.getTileLength();     
        for (int i = 0; i < tilesToPlot.length; i++) {
            int w = tilesToPlot[i].x;
            int l = tilesToPlot[i].y;
            g.drawImage(dir.getTile(w, l), w * width, l * height, width, height, null);
        }
    }
            
    public BufferedImage getSubImage(Rectangle bounds) {
        TiffDirectoryBuffer dir = getBaseDirectoryBuffer();
        return getSubImage(bounds, dir );
    }
        
    public double percentBuffered(TiffDirectoryBuffer dir, Point tiles[]){
        int count = 0;
        for (Point tile : tiles) {
            int w = tile.x;
            int l = tile.y;
            if (dir.getTileFechingStatus(w, l) == BufferingStatus.BUFFERED){
                count++;
            }
        }    
        return count/(tiles.length);
    }
    
    public Point[] findTilesRect(TiffDirectoryBuffer dir, Rect r) {       
        int lxt = r.getLx()/dir.getTileWidth();
        int lyt = r.getLy()/dir.getTileLength();
        int uxt = Math.min(dir.getTilesAcrossW()-1,r.getUx()/dir.getTileWidth());
        int uyt = Math.min(dir.getTilesDownL()-1,r.getUy()/dir.getTileLength());       
        
        int dx = uxt - lxt +1;
        int dy = uyt - lyt +1;
        
        if ((dx < 1)||(dy < 1)) {
            return null;
        }

        Point tilesToPlot[] = new Point[dx*dy];
        for (int i = 0; i < dx; i++) {
            for (int j = 0; j < dy; j++) {
                tilesToPlot[i * dy + j] = new Point(lxt + i, lyt + j);
            }
        }
        return tilesToPlot;
    } 
    
    public static SlideSource getSlideSourceForHash(ArrayList<SlideSource> slideSources, byte hash[]) {
        for (SlideSource slideSource : slideSources) {
            if (ImageID.hashesAreEqual(hash, slideSource.getSlideReference().hash)) {
                return slideSource;
            }
        }
        return null;
    }
    
}
